using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Net;
using System.Reflection;
using System.Web.Services.Description;
using System.Xml.Schema;
using System.Xml.Serialization;
using Microsoft.CSharp;
using Sixeyed.CodeGeneration.Metadata.Bases;
using Sixeyed.CodeGeneration.Metadata.Wsdl.Interfaces;
using Sixeyed.CodeGeneration.Metadata.Wsdl.Providers;

namespace Sixeyed.CodeGeneration.Metadata.Wsdl
{
    /// <summary>
    /// Abstract base class for WSDL sources
    /// </summary>
    public abstract class WsdlSource : SourceBase, IWsdlSource
    {
        #region Private instance fields

        private Assembly _proxyAssembly;
        private ServiceDescription _serviceDescription = null;
        private List<MethodInfo> _webMethods;

        #endregion

        #region Instance properties

        /// <summary>
        /// Gets the name of the source type
        /// </summary>
        public override string SourceTypeName
        {
            get { return "WSDL"; }
        }

        /// <summary>
        /// Gets the communication protocol used by the source
        /// </summary>
        protected virtual string ProtocolName
        {
            get { return "Soap"; }
        }

        /// <summary>
        /// Gets the formatted Uri for WSDL queries
        /// </summary>
        private string WsdlUri
        {
            get
            {
                string uri = this.Uri;
                if (!uri.EndsWith("?wsdl"))
                {
                    uri = string.Format("{0}?wsdl", uri);
                }
                return uri;
            }
        }

        /// <summary>
        /// Gets/sets the Uri for the WSDL endpoint
        /// </summary>
        public override string Uri
        {
            get { return base.Uri; }
            set
            {
                if (base.Uri != value)
                {
                    base.Uri = value;
                    this._serviceDescription = null;
                }
            }
        }

        /// <summary>
        /// Gets the service description for the endpoint
        /// </summary>
        protected ServiceDescription ServiceDescription
        {
            get
            {
                if (this._serviceDescription == null)
                {
                    this._serviceDescription = GetServiceDescription();
                }
                return this._serviceDescription;
            }
        }

        /// <summary>
        /// Gets a proxy assembly for the endpoint description
        /// </summary>
        protected Assembly ProxyAssembly
        {
            get
            {
                if (this._proxyAssembly == null)
                {
                    this._proxyAssembly = this.GenerateProxyAssembly();
                }
                return this._proxyAssembly;
            }
        }

        /// <summary>
        /// Gets the Web methods for the endpoint
        /// </summary>
        protected List<MethodInfo> WebMethods
        {
            get
            {
                if (this._webMethods == null)
                {
                    this._webMethods = new List<MethodInfo>();
                    string serviceName = this.ServiceDescription.Services[0].Name;
                    Type service = this.ProxyAssembly.GetType(serviceName);
                    this._webMethods.AddRange(service.GetMethods(BindingFlag.All));
                }
                return this._webMethods;
            }
        }

        #endregion

        #region Instance methods

        /// <summary>
        /// Returns the service description of the endpoint
        /// </summary>
        /// <returns>Populated <see cref="System.Web.Services.Description.ServiceDescription"/></returns>
        protected virtual ServiceDescription GetServiceDescription()
        {
            WebRequest request = WebRequest.Create(this.WsdlUri);
            Stream requestStream = request.GetResponse().GetResponseStream();
            return ServiceDescription.Read(requestStream);
        }

        /// <summary>
        /// Returns available schemas
        /// </summary>
        /// <returns>List of schema names</returns>
        public List<string> GetSchemaNames()
        {
            List<string> schemaNames = new List<string>(this.ServiceDescription.Types.Schemas.Count);
            foreach (XmlSchema schema in this.ServiceDescription.Types.Schemas)
            {
                schemaNames.Add(schema.TargetNamespace);
            }
            return schemaNames;
        }

        /// <summary>
        /// Returns the schema for the given target namespace
        /// </summary>
        /// <param name="targetNamespace">Namespace of the schema to load</param>
        /// <returns>Populated <see cref="System.Xml.Schema.XmlSchema"/></returns>
        public XmlSchema GetSchema(string targetNamespace)
        {
            XmlSchema xmlSchema = null;
            foreach (XmlSchema schema in this.ServiceDescription.Types.Schemas)
            {
                if (schema.TargetNamespace == targetNamespace)
                {
                    xmlSchema = schema;
                    break;
                }
            }
            return xmlSchema;
        }

        /// <summary>
        /// Generates a proxy assembly for the service endpoint
        /// </summary>
        /// <returns>Populated <see cref="System.Reflection.Assembly"/></returns>
        private Assembly GenerateProxyAssembly()
        {
            Assembly proxy = null;

            //import service description from the endpoint:
            ServiceDescriptionImporter importer = new ServiceDescriptionImporter();
            importer.AddServiceDescription(this.ServiceDescription, String.Empty, String.Empty);
            importer.ProtocolName = this.ProtocolName;
            importer.CodeGenerationOptions = CodeGenerationOptions.GenerateProperties;

            //set up code generation:
            CodeNamespace codeNamespace = new CodeNamespace();
            CodeCompileUnit compileUnit = new CodeCompileUnit();
            compileUnit.Namespaces.Add(codeNamespace);
            ServiceDescriptionImportWarnings warnings = importer.Import(codeNamespace, compileUnit);

            //if methods found, generate the code:
            if (warnings != ServiceDescriptionImportWarnings.NoCodeGenerated &&
                warnings != ServiceDescriptionImportWarnings.NoMethodsGenerated)
            {
                CSharpCodeProvider provider = new CSharpCodeProvider();
                StringWriter sw = new StringWriter(CultureInfo.CurrentCulture);
                provider.GenerateCodeFromNamespace(codeNamespace, sw, null);

                //and create the assembly:
                CompilerParameters param = new CompilerParameters(new String[] { "System.dll", "System.Xml.dll", "System.Web.Services.dll", "System.Data.dll" });
                param.GenerateExecutable = false;
                param.GenerateInMemory = false;
                param.TreatWarningsAsErrors = false;
                param.WarningLevel = 4;
                CompilerResults results = new CompilerResults(null);
                results = provider.CompileAssemblyFromSource(param, sw.ToString());
                proxy = results.CompiledAssembly;
            }
            return proxy;
        }

        /// <summary>
        /// Returns a list of Web methods
        /// </summary>
        /// <returns>List of Web method names</returns>
        public List<string> GetWebMethodNames()
        {
            List<string> methodNames = new List<string>(this.WebMethods.Count);
            foreach (MethodInfo webMethod in this.WebMethods)
            {
                methodNames.Add(webMethod.Name);
            }
            return methodNames;
        }

        /// <summary>
        /// Returns a Web method for the given method name
        /// </summary>
        /// <param name="methodName">Name of method to return</param>
        /// <returns>Web method</returns>
        public MethodInfo GetWebMethod(string methodName)
        {
            MethodInfo webMethod = null;
            foreach (MethodInfo method in this.WebMethods)
            {
                if (method.Name == methodName)
                {
                    webMethod = method;
                    break;
                }
            }
            return webMethod;
        }

        #endregion

        #region Structs

        /// <summary>
        /// Struct used to centralise binding flags
        /// </summary>
        private struct BindingFlag
        {
            public const BindingFlags All = BindingFlags.DeclaredOnly | BindingFlags.IgnoreCase |
                                            BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.Public;
        }

        #endregion
    }
}